Category & Extension

对于什么是Category,我觉得还是从其作用上将应该更容易理解。

由于某些原因发现现有的类的方法特别是系统自带的类无法满足现有的情况,那怎么办呢?这个时候Category就出现了。因为Category可以在不改变原有类的结果情况下添加自己自定义的方法。需要注意的是Category原则上只能添加方法而不能添加属性。为什么是原则上情况呢?下文会逐步讲解。

Category 有分类类别两种叫法。

上面只是笼统的讲述了Category,下面来逐个讲解相关功能。

一:添加方法

首页创建一个Category。此处创建了一个UIColor的分类Default,并自定义一个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// .h 添加一个类方法
@interface UIColor (Default)

+ (UIColor *)defaultColorForButtonTtitle;

@end

// .m 实现这个类方法
+ (UIColor *)defaultColorForButtonTtitle {
UIColor *color = [[UIColor alloc] init];
color = UIColor.redColor;
return color;
}

// 类方法调用
[pushbut setTitleColor:[UIColor defaultColorForButtonTtitle] forState:UIControlStateNormal];

这就是Category方法的添加。因为可以自定义自己的方法,由此可以推出第二个作用。

二:将类的实现代码分散到多个分类当中

因为类中经常容易填满各种方法,而这些方法的代码则全部在一个巨大的实现文件里。使用Category的机制,可以把代码按逻辑划入几个分区中,以便开发和调试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface PresonInfo : NSObject

@property (nonatomic, copy) NSString *familyName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, strong) NSArray *friends;

- (void)initWithFamilyName:(NSString *)firstName andLastName:(NSString *)lastName;

- (void)addBoyFriend:(PresonInfo *)preson;
- (void)deleteBoyFriend:(PresonInfo *)preson;

- (void)addGirlFriend:(PresonInfo *)preson;
- (void)deleteGirlFriend:(PresonInfo *)preson;

- (void)addALLFriend:(PresonInfo *)preson;
- (void)deleteALLFriend:(PresonInfo *)preson;

@end

PresonInfo类的方法可能并不算多但也都是在.m文件中要实现的,那如果后期还要继续添加方法那务必会造成.m文件代码量的变大。时间长了自己都忘了改方法的具体作用。这时Category就起到了很大的作用。如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@interface PresonInfo : NSObject

@property (nonatomic, copy) NSString *familyName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, strong) NSArray *friends;


- (void)initWithFamilyName:(NSString *)firstName andLastName:(NSString *)lastName;

@end

// boy
@interface PresonInfo (Boy)

- (void)addBoyFriend:(PresonInfo *)preson;
- (void)deleteBoyFriend:(PresonInfo *)preson;

@end

// girl
@interface PresonInfo (Girl)

- (void)addGirlFriend:(PresonInfo *)preson;
- (void)deleteGirlFriend:(PresonInfo *)preson;

@end

// all
@interface PresonInfo (All)

- (void)addALLFriend:(PresonInfo *)preson;
- (void)deleteALLFriend:(PresonInfo *)preson;

@end
  • 然而在创建分类添加方法时,往往都会出现某些方法是比较私有的。我们可以把这些比较私有的方法单独并归到一个分类中,这样就可以隐藏实现细节。

三:使用class-continuation分类 隐藏实现文件

就如上面说的类中经常会包含一些无须对外公布的方法和实例变量。然而Objective-C的动态消息机制又实现不了真正的方法私有或者实例变量的私有,但是还要有必要隐藏着部分的内容。着就是class-continuation分类存在的意义。

  • class-continuation分类必须定义在其所接续的那个类的实现文件里。原因在于这是唯一能声明实例变量的分类,而且此分类没有特定的实现文件,其中的方法都应该定义在类的主实现文件里
1
2
3
4
// class-continuation分类写法
@interface PresonInfo ()
// Methods
@end
  • 这样就可以将其隐藏起来,只供本类使用。

四:关联对象

关联对象:Associated Object,即把一个对象关联到另一个对象上。

在讲解管理对象之前先来讲讲Category的属性相关问题。
都知道在一般类中创建一个属性,都会生成对应的实例变量,getter方法和setter方法。这些都是编译器自动为我们生成的。然而为什么在Category里添加属性会有警报甚至crash呢?那是因为在Category里添加的属性是不会自动生成对应的成员变量以及getter和setter的实现方法

既然不能自动生成那可以手动设置吗?显然是可以的。此时会涉及到另一个十分重要的知识点Runtime(在此只讲解本文相关的知识点,后续会有专门篇幅)。通过Runtime的相关API就能手动的给Category添加对应的getter和setter的实现方法。其实这个过程也就是关联对象的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 当我在UIColor的分类Default中添加下面一个属性
@interface UIColor (Default)

@property (nonatomic, copy) NSString *nameString;

+ (UIColor *)defaultColorForButtonTtitle;

@end

// 在.m文件中就会出现这么一条警告
Property 'nameString' requires method 'nameString' to be defined - use @dynamic or provide a method implementation in this category

// 大致意思是提醒我们需要定义该属性实例变量的方法,使用@dynamic或者自己定义。

@dynamic:告诉编译器属性的getter和setter方法由用户实现,不自动生成。所以最后我们还是得手动来设置方法。

下面就来手动实现先如何关联对象。

1
2
3
4
5
6
7
8
9
// getter方法
- (NSString *)name {
return objc_getAssociatedObject(self, @"nameKey");
}

// setter方法
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, @"nameKey", name, OBJC_ASSOCIATION_COPY);
}
1
2
3
4
5
6
7
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)

// object : 关联的源对象 key : 关联的键值

objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,id _Nullable value, objc_AssociationPolicy policy)

// object : 关联的源对象 key : 关联的键值 value : 关联的对象 objc_AssociationPolicy policy : 关联的策略
1
2
3
4
// 调用 (可能不是那么的符合实际)
UIColor *color = [[UIColor alloc] init];
color.name = @"红色";
[pushbut setTitle:color.name forState:UIControlStateNormal];

这样就实现了给分类添加一个属性。其中也是Runtime的功能之一。

然而对于关联对象Runtime还提供了一个移除的API

1
2
// 这个其实应很容易理解了
void objc_removeAssociatedObjects(id object)

因为可以自定义方法那必然会发生一些冲突,比如自定义的方法跟原有类的方法一致会产生什么影响?

1
2
3
4
5
6
7
8
// 在UIColor分类中添加了如下方法 该方法跟UIColor类原有方法一致
@interface UIColor (Default)

@property (nonatomic, copy) NSString *name;

+ (UIColor *)colorWithWhite:(CGFloat)white alpha:(CGFloat)alpha;

@end

这时在.m文件中就会出现这个么一个警告

1
2
Category is implementing a method which will also be implemented by its primary class
// 意思是原有类已经有这个方法
在Category中存在着这样的方法调用优先级 分类 -> 本类 -> 父类, 当实现并调用这个方法时因iOS方法调用机制的原理,找到了相对应的方法名后就直接调用了并停止继续寻找其他相对应的方法名。这样原有类的方法就被忽略了。
  • 因此在开发的过程中尽量不要在Category中覆盖原有类方法,如真的有必要则改用继承。

Extension

对于Extension之前一直都不是很清楚什么是Extension,然而当贴出下面这段代码立马就眼前一亮。因为这种形式的代码在平时用的是不能再少了。当然Extension还有其他的表现形式,比如单独创建一个扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// .m 文件  @interface 部分的代码就是Extension
@interface Test_ViewController ()
{
NSString *string;
NSArray *array;
NSDictionary *dictionary;
}

@property (nonatomic, strong) UIButton *push;

@end


@implementation Test_ViewController

@end

单独添加扩展

Extension(扩展、延展、匿名分类)跟Category最大的区别在于Extension不仅仅能声明方法,还可以声明属性及成员变量。不过这些都是私有的,只有在当前类中执行。上面说到Category应该避免覆盖原有类的方法,然而Extension因为没有对应的实现文件。只是提供了相应的方法个属性,即使实现了自定义的同名方法其实也相当于重置了原有类的方法,运行后也相当于调用了原有类的方法,而不是覆盖。

区别

1.Category只可以添加方法,不自带增加成员变量。Extension则都可以。

2.Category可以访问原有类的.h文件中的属性,而Extension不行。

3.Category会覆盖原有类的方法调用,而Extension相当于重置。

那接下来就讲讲本文中提到的Runtime